<?php

namespace Gnway\Ticket\Rest\Mapping;

use Gnway\Ticket\Rest\Container\RestContainer;
use Gnway\Ticket\Rest\Contracts\ValidateRuleInterface;
use Gnway\Ticket\Rest\Exceptions\RouteAddErrorException;
use Gnway\Ticket\Rest\Exceptions\RouteMethodNotSupportException;
use Gnway\Ticket\Rest\Request\RequestMethod;
use Gnway\Ticket\Rest\Util\FunctionUtils;
use ReflectionException, RuntimeException, Closure;

/**
 * Class RouterMapping
 *
 * @package Gnway\Ticket\Rest\Route
 */
class RouterMapping
{

  /**
   * Request Mappings
   *
   * @var array
   */
  protected array $requestMappings = [];

  /**
   * Allowed Methods
   *
   * @var array
   */
  protected array $allowedMethods = [];

  /**
   * Prefix name
   *
   * @var string
   */
  protected string $tmpPrefix = '';

  /**
   * Suffix name
   *
   * @var string
   */
  protected string $tmpSuffix = '';

  /**
   * Last uris
   *
   * @var array
   */
  protected array $lastUris = [];

  /**
   * Bind parameter rule map
   *
   * @var array
   */
  protected array $bindParameterRuleMap = [];

  /**
   * RouterMapping constructor.
   */
  public function __construct()
  {
    $this->allowedMethods = RequestMethod::getConstants();
  }

  /**
   * Add Route Mapping
   *
   * @param array  $methods
   * @param string $uri
   * @param array|string|callable $action
   * @return $this
   * @throws RouteMethodNotSupportException
   * @throws RuntimeException
   */
  public function addRoute(array $methods, string $uri, $action): RouterMapping
  {
    //Validate request methods
    $this->validateMethods($methods);

    //Delete illegal symbols
    $uri = trim($uri, " \t\n\r\0\x0B\\");

    //Validate request uri and target action
    $this->validateActionAndURI($uri, $action);

    //Delete both "/" symbol
    $uri = '/' . trim($uri, '/');

    //Set prefix
    if ($this->tmpPrefix !== '') {
      if ($uri !== '/') {
        $uri = $this->tmpPrefix !== '/' ? $this->tmpPrefix . $uri : $uri;
      } else {
        $uri = $this->tmpPrefix;
      }
    }

    //Set suffix
    if ($uri !== '/' && $this->tmpSuffix !== '') {
      $uri .= $this->tmpSuffix;
    }

    //Reset last uris
    $this->lastUris = [];

    $methods = array_unique($methods);
    foreach ($methods as $method) {
      $key = strtolower($method) . $uri;
      if (isset($this->requestMappings[$key])) {
        throw new RouteAddErrorException('Route mapping "' . $key . '" already exists');
      }
      $this->requestMappings[$key] = $action;

      $this->lastUris[] = $key;
    }

    return $this;
  }

  /**
   * Validate Input Methods
   *
   * @param array $methods
   * @return void
   */
  protected function validateMethods(array $methods): void
  {
    if (empty($methods)) {
      throw new RouteAddErrorException('Request method cannot be empty');
    }

    $notAllowed = array_diff($methods, $this->allowedMethods);
    if (sizeof($notAllowed) > 0) {
      throw new RouteMethodNotSupportException(sprintf('Route method supports only values is %s, %s given',
        json_encode($this->allowedMethods), json_encode($notAllowed)));
    }
  }

  /**
   * Validate request uri and target action
   *
   * @param string $uri
   * @param mixed  $action
   * @return void
   */
  protected function validateActionAndURI(string $uri, $action): void
  {
    if ($uri === '') {
      throw new RouteAddErrorException('Request URI cannot be empty');
    }
    if (!( (is_array($action) && isset($action[0], $action[1]))
      || (is_string($action) && (strpos($action, '@') || function_exists($action)))
      || $action instanceof Closure )) {
      throw new RouteAddErrorException('Router action invalid');
    }
  }

  /**
   * Get Request Mappings
   *
   * @return array
   */
  public function getRequestMappings(): array
  {
    return $this->requestMappings;
  }

  /**
   * Get Bind Parameter Rule Map
   *
   * @return array
   */
  public function getBindParameterRuleMap(): array
  {
    return $this->bindParameterRuleMap;
  }

  /**
   * Copy mappings
   *
   * @param RouterMapping|null $from
   * @return void
   */
  public function copy(?RouterMapping $from): void
  {
    if ($from !== NULL) {
      $this->requestMappings = $from->getRequestMappings();
      $this->bindParameterRuleMap = $from->getBindParameterRuleMap();
    }
  }

  /**
   * GET
   *
   * @param string $uri
   * @param array|string|Closure $action
   * @return RouterMapping
   */
  public function get(string $uri, $action): RouterMapping
  {
    return $this->addRoute([RequestMethod::HEAD, RequestMethod::GET], $uri, $action);
  }

  /**
   * POST
   *
   * @param string $uri
   * @param array|string|Closure $action
   * @return RouterMapping
   */
  public function post(string $uri, $action): RouterMapping
  {
    return $this->addRoute([RequestMethod::POST], $uri, $action);
  }

  /**
   * PUT
   *
   * @param string $uri
   * @param array|string|Closure $action
   * @return RouterMapping
   */
  public function put(string $uri, $action): RouterMapping
  {
    return $this->addRoute([RequestMethod::PUT], $uri, $action);
  }

  /**
   * DELETE
   *
   * @param string $uri
   * @param array|string|Closure $action
   * @return RouterMapping
   */
  public function delete(string $uri, $action): RouterMapping
  {
    return $this->addRoute([RequestMethod::DELETE], $uri, $action);
  }

  /**
   * HEAD
   *
   * @param string $uri
   * @param array|string|Closure $action
   * @return RouterMapping
   */
  public function head(string $uri, $action): RouterMapping
  {
    return $this->addRoute([RequestMethod::HEAD], $uri, $action);
  }

  /**
   * Group
   *
   * @param string  $prefix
   * @param Closure $closure
   * @param string  $suffix
   * @return RouterMapping
   * @throws ReflectionException
   */
  public function group(string $prefix, Closure $closure, string $suffix = ''): RouterMapping
  {
    $prefix = trim($prefix, " \t\n\r\0\x0B\\");
    if ($prefix !== '') {
      $prefix = '/' . trim($prefix, '/');
      if ($this->tmpPrefix !== '' && $this->tmpPrefix !== '/') {
        $this->tmpPrefix .= $prefix;
      } else {
        $this->tmpPrefix = $prefix;
      }
    }

    $suffix = trim($suffix, " \t\n\r\0\x0B\\.");
    if ($suffix !== '') {
      if ($this->tmpSuffix !== '') {
        $this->tmpSuffix .= '.' . $suffix;
      } else {
        $this->tmpSuffix = '.' . $suffix;
      }
    }

    FunctionUtils::invoke($closure);

    $this->tmpPrefix = '';
    $this->tmpSuffix = '';

    return $this;
  }

  /**
   * Set bind parameters validation
   *
   * @param string|array $name
   * @param string       $rule
   * @return $this
   */
  public function validation($name, string $rule = ''): RouterMapping
  {
    if (is_array($name) && $name) {
      foreach ($name as $k => $v) {
        $this->validation($k, $v);
      }
    } elseif (is_string($name) && $name && $rule) {
      if (class_exists($rule) && is_subclass_of($rule, ValidateRuleInterface::class)) {
        try {
          $rule = RestContainer::make($rule)->getValue();
          if (empty($rule))
            return $this;
        } catch (ReflectionException $e) {
          return $this;
        }
      }
      foreach ($this->lastUris as $uri) {
        $this->bindParameterRuleMap[$uri][$name] = $rule;
      }
    }

    return $this;
  }
}